"""Classes that deal with managing and scoring what's on the board."""

__author__ = "Gabriel Taubman (gtaubman@mac.com)"

import random
import unittest
from constants import BLANK_TILE, TILES_IN_HAND, BINGO_BONUS
from move import Move, InvalidMoveError
from word_list import WordList, InfiniteWordList

class BoardSquare:
  """A class representing a position on the board, any letter resting on that
  square, and the square's score multipliers.

  A piece on a scrabble board can either multiply the score of the letter placed
  onto it, or it can multiply the score of the entire word which covers this
  square.  This class supports a board square that does both, but that doesn't
  exist in the standard Scrabble game.
  """
  def __init__(self, word_multiplier = 1, letter_multiplier = 1):
    self.word_multiplier = word_multiplier
    self.letter_multiplier = letter_multiplier
    self.letter = ""

  def Letter(self):
    """Returns the letter occupying this board square."""
    return self.letter

  def SetLetter(self, letter):
    """Sets the letter occupying this board square."""
    self.letter = letter.upper()

  def Empty(self):
    """Returns whether or not this board square has a letter."""
    return self.letter == ""

  def WordMultiplier(self):
    """Returns the word multiplier for this board square."""
    return self.word_multiplier

  def SetWordMultiplier(self, word_multiplier):
    """Sets the word multiplier for this board square."""
    self.word_multiplier = word_multiplier

  def LetterMultiplier(self):
    """Returns the letter multiplier for this board square."""
    return self.letter_multiplier

  def SetLetterMultiplier(self, letter_multiplier):
    """Sets the letter multiplier for this board square."""
    self.letter_multiplier = letter_multiplier

  def __str__(self):
    if self.letter:
      return "[ %s ]" % self.letter
    return "[   ]"


class IllegalBoardPositionError(Exception):
  """Raised if a board position is requested that does not exist."""
  def __init__(self, position):
    self.position = position

  def __str__(self):
    return repr(self.position)


class Board:
  """A representation of the scrabble board.

  The standard representation of a scrabble board, with the normal set of piece
  and word multipliers:

  [W 3][   ][   ][L 2][   ][   ][   ][W 3][   ][   ][   ][L 2][   ][   ][W 3]
  [   ][W 2][   ][   ][   ][L 3][   ][   ][   ][L 3][   ][   ][   ][W 2][   ]
  [   ][   ][W 2][   ][   ][   ][L 2][   ][L 2][   ][   ][   ][W 2][   ][   ]
  [L 2][   ][   ][W 2][   ][   ][   ][L 2][   ][   ][   ][W 2][   ][   ][L 2]
  [   ][   ][   ][   ][W 2][   ][   ][   ][   ][   ][W 2][   ][   ][   ][   ]
  [   ][L 3][   ][   ][   ][L 3][   ][   ][   ][L 3][   ][   ][   ][L 3][   ]
  [   ][   ][L 2][   ][   ][   ][L 2][   ][L 2][   ][   ][   ][L 2][   ][   ]
  [W 3][   ][   ][L 2][   ][   ][   ][W 2][   ][   ][   ][L 2][   ][   ][W 3]
  [   ][   ][L 2][   ][   ][   ][L 2][   ][L 2][   ][   ][   ][L 2][   ][   ]
  [   ][L 3][   ][   ][   ][L 3][   ][   ][   ][L 3][   ][   ][   ][L 3][   ]
  [   ][   ][   ][   ][W 2][   ][   ][   ][   ][   ][W 2][   ][   ][   ][   ]
  [L 2][   ][   ][W 2][   ][   ][   ][L 2][   ][   ][   ][W 2][   ][   ][L 2]
  [   ][   ][W 2][   ][   ][   ][L 2][   ][L 2][   ][   ][   ][W 2][   ][   ]
  [   ][W 2][   ][   ][   ][L 3][   ][   ][   ][L 3][   ][   ][   ][W 2][   ]
  [W 3][   ][   ][L 2][   ][   ][   ][W 3][   ][   ][   ][L 2][   ][   ][W 3]

  where "W 2" means double word, and "L 3" means triple letter.
  
  Internally, it uses a list of lists representation.  However, most of the
  functions operate in "board space" where the * square is considered to be
  (0,0) and translation is done from that space to the list index space.

  Args:
    wordlist: A WordList for determining when invalid moves are played.  If none
              is specified, all words are allowed.
  """

  def __init__(self, wordlist = None):
    # If no wordlist is specified, use an infinite one.
    if wordlist == None:
      self.wordlist = InfiniteWordList()
    else:
      self.wordlist = wordlist

    # The standard scrabble board consists of a 15x15 grid.
    self.board = [[BoardSquare() for i in range(15)] for j in range(15)]

    # Apply the letter multipliers.
    double_letter = [(1,1), (4,0), (5,1), (7,4)]
    triple_letter = [(2,2), (6,2)]

    for x, y in double_letter:
      self._SetSymmetricalMultipliers((x, y), word = 1, letter = 2)
    for x, y in triple_letter:
      self._SetSymmetricalMultipliers((x, y), word = 1, letter = 3)

    # Apply the word multipliers.
    double_word = [(0,0), (3,3), (4,4), (5,5), (6,6)]
    triple_word = [(7,7), (0,7)]

    for x, y in double_word:
      self._SetSymmetricalMultipliers((x, y), word = 2, letter = 1)
    for x, y in triple_word:
      self._SetSymmetricalMultipliers((x, y), word = 3, letter = 1)

  def Empty(self):
    """A board is empty if all of its pieces are empty."""
    for row in self.board:
      for square in row:
        if not square.Empty():
          return False
    return True

  def ScoreForMove(self, move):
    """Returns the score for the specified move.  Does no word validation.

    Raises an InvalidMoveError if the move is not valid on the board.
    Args:
      move: A Move object containing the desired letters and locations to play. 

    Returns:
      An integer score for the specified word.
    """
    # The move must be valid if we're going to score it.
    if not move.Valid():
      raise InvalidMoveError("Can't score an invalid move.")

    # Make sure none of the BoardSquares that the move is attempting to place
    # tiles in are already occupied.
    for position in move.IterPositions():
      if not self[position].Empty():
        move.valid = False
        raise InvalidMoveError("Attempted to place a tile %s ontop of another."
                               % repr(position))

    # If this is the first move, then it has to contain (0,0).
    if self.Empty() and not move.UsesPosition((0,0)):
      move.valid = False
      raise InvalidMoveError("First move must use (0,0).  Not %s" %
          move.tiles)

    # If it's not the first move, one of this move's pieces needs to be adjacent
    # to a piece already on the board.
    if not self.Empty():
      found_one_piece = False
      for position in move.IterPositions():
        if len(self._SquaresAroundPosition(position)) != 0:
          found_one_piece = True
          break;
      if not found_one_piece:
        move.valid = False
        raise InvalidMoveError("Move must touch at least one piece.")

    # Make sure that there is an uninterrupted line of pieces from the beginning
    # to the end of the move.  This prohibits moves such as:
    #   Before [C][ ][T][ ][H][ ][T] 
    #   After  [C][A][T][ ][H][A][T] 
    start_position = list(move.LeftmostPosition())
    end_position = list(move.RightmostPosition())
    if move.Direction() == Move.HORIZONTAL:
      for i in range(start_position[0], end_position[0] + 1):
        test_pos = (i, start_position[1])
        if self[test_pos].Empty() and not move.UsesPosition(test_pos):
          move.valid = False
          raise InvalidMoveError(
              "There must be continuous pieces from %s to %s.  %s was empty." %
              (repr(start_position), repr(end_position), repr(test_pos)))
    elif move.Direction() == Move.VERTICAL:
      for i in range(end_position[1], start_position[1] + 1):
        test_pos = (start_position[0], i) 
        if self[test_pos].Empty() and not move.UsesPosition(test_pos):
          move.valid = False
          raise InvalidMoveError(
              "There must be continuous pieces from %s to %s.  %s was empty." %
              (repr(start_position), repr(end_position), repr(test_pos)))
    else:
      # It's a one-piece move.  We just have to check that at least one of the
      # squares adjacent to it isn't empty.
      pos = move.Position()
      if len(self._SquaresAroundPosition(pos)) == 0:
        move.valid = False
        raise InvalidMoveError("You can't place just one tile at %s." %
            repr(pos))

    # Scoring algorithm:
    # For the direction the move is in, pick one piece and calculate a score of
    # the pieces to either direction (for horizontal, left right, for vertical,
    # up down) and of the piece itself.  This includes letter multipliers for
    # any new pieces, and word multipliers for the finished word.
    #
    # Next, go through all the pieces in the move and do the same thing but for
    # the opposite orientation, so if horizontal, check the up down of every
    # piece in the move, for vertical, check the left right of every piece in
    # the move.
    score_along = self._ScorePosition(move, move.Position(), move.Direction())
    score_against = 0
    for pos in move.IterPositions():
      score_against += self._ScorePosition(move, pos, move.OppositeDirection())

    # If this is a valid move (has a non zero score) and it used all the letters
    # in a player's hand, give an extra boost.
    if move.NumTiles() == TILES_IN_HAND and score_along + score_against > 0:
      bingo_boost = BINGO_BONUS
    else:
      bingo_boost = 0
    return score_along + score_against + bingo_boost

  def RandomPiece(self):
    """Returns a random piece on the board.  Returns none if there are none."""
    return self[self.RandomPosition()]

  def RandomPosition(self):
    """Returns an (x,y) tuple of a random filled tile on the board.
    
    If there are no filled tiles, this returns None."""
    if self.Empty():
      return None

    positions = []
    edge_length = len(self.board) - 1
    for j in range(-edge_length / 2, edge_length / 2 + 1):
      for i in range(-edge_length / 2, edge_length / 2 + 1):
        if not self[(i,j)].Empty():
          positions.append((i, j))
    return random.choice(positions)

  def ValidPositions(self):
    """Returns a set of positions which border filled squares."""
    s = set()
    edge_length = len(self.board) - 1
    for j in range(-edge_length / 2, edge_length / 2 + 1):
      for i in range(-edge_length / 2, edge_length / 2 + 1):
        if self[(i,j)].Empty() and len(self._SquaresAroundPosition((i,j))):
          s.add((i,j))
    return s

  def LeftOfPosition(self, position):
    """Returns the board square to the left of the specified position.

    Returns None if that would be a tile not on the board.
    """
    try:
      return self[(position[0] - 1, position[1])]
    except IllegalBoardPositionError:
      pass
    return None

  def RightOfPosition(self, position):
    """Returns the board square to the right of the specified position.

    Returns None if that would be a tile not on the board.
    """
    try:
      return self[(position[0] + 1, position[1])]
    except IllegalBoardPositionError:
      pass
    return None

  def AbovePosition(self, position):
    """Returns the board square to the above the specified position.

    Returns None if that would be a tile not on the board.
    """
    try:
      return self[(position[0], position[1] + 1)]
    except IllegalBoardPositionError:
      pass
    return None

  def BelowPosition(self, position):
    """Returns the board square to the below the specified position.

    Returns None if that would be a tile not on the board.
    """
    try:
      return self[(position[0], position[1] - 1)]
    except IllegalBoardPositionError:
      pass
    return None

  def BoardHash(self):
    """Returns a string containing an encoded board.
    
    Included in this encoding are the letters on the board, the word
    multipliers, and the letter multipliers.
    """
    to_return = []
    for row in self.board:
      s = []
      for piece in row:
        s.append("%s%d%d" % (piece.letter,
                             piece.word_multiplier,
                             piece.letter_multiplier))
      to_return.append("".join(s))
    return "".join(to_return)

  def _ScorePosition(self, move, position, direction):
    """Returns the score from a position.
    
    Args:
      move: A Move object describing the move to score.
      position: An (x,y) tuple of where to start scoring.  It should always be a
                position held by the move, not by the board.
      direction: One of Move.HORIZONTAL, Move.VERTICAL or Move.NO-DIRECTION.

    Returns:
      An integer value of the score for this position.

    There are two ways of scoring a single position: "along" and "against".
    What they do depends on the type of move being scored.

    "Along" means horizontally for a horizontal move, and vertically for a
    vertical or no-direction move.  For example, if a player played "EAR":

        _G_
        _A_     _C_
        _B_     _A_
    _H_ [E] [A] [R]

    Scoring "along" this move would tally the score for "HEAR" and would return
    the same score for [E] [A] or [R].

    Scoring "against" a move means vertically for a horizontal move, and
    horizontally for a vertical or no-direction move.  Given the same example
    where the player is playing "EAR":

        _G_
        _A_     _C_
        _B_     _A_
    _H_ [E] [A] [R]

    Scoring "against" this move, given the position of "E" would return a score
    for "GABE", given the position of "A" would return a score of 0, and the
    position of "R" would return a score for "CAR".

    In order to score against a move, pass the opposite direction of the move.
    So, if the move is a horizontal move, passing vertical will score against
    it.
    """
    # We pick a tile from the move, and walk in either direction.
    start_position = list(position)

    # First we'll walk "forwards".
    if move.Direction() == Move.HORIZONTAL:
      if direction == move.Direction():
        direction = (1, 0)
      else:
        direction = (0, -1)
    else:
      if direction == move.Direction():
        direction = (0, -1)
      else:
        direction = (1, 0)

    f_score, f_word_multipliers, f_letters_used = \
        self._SumOfTilesFromPositionInDirection(move, start_position, direction)

    # Now we walk backwards.
    direction = (direction[0] * -1, direction[1] * -1)

    b_score, b_word_multipliers, b_letters_used = \
        self._SumOfTilesFromPositionInDirection(move, start_position, direction)

    # Finally, we get the score of our start position.
    s_score = (Board.score_for_letter[move.LetterAtPosition(start_position)] *
               self[start_position].LetterMultiplier())
    s_word_multipliers = self[start_position].WordMultiplier()
    s_letter_used = move.LetterAtPosition(start_position)

    # A single piece by itself never counts, so if there's no score besides the
    # single piece case, we return 0 score.
    if f_score + b_score == 0:
      return 0

    # Assemble the final played word and make sure it exists in our WordList.
    combined_word = "".join([b_letters_used[::-1],
                             s_letter_used,
                             f_letters_used])
    if not self.wordlist.IsValidWord(combined_word):
      move.valid = False
      raise InvalidMoveError("%s is not a word." % combined_word)

    # Return our final score.
    return ((f_word_multipliers * b_word_multipliers * s_word_multipliers) *
            (f_score + b_score + s_score))

  def _SumOfTilesFromPositionInDirection(self, move, position, direction):
    """Returns a tuple of (score, word_multipliers, letters_used).
    
    Given a move, an (x,y) position from that move, and an (x,y) normal
    direction, this walks along the board starting at _one past_ the position in
    the direction specified, accumulating the score from tiles already on the
    board, and tiles in the move.  As soon as it encounters a board square which
    is not included in the move and is empty on the board, it's done.  For tiles
    in the move, the word multiplier is accumulated and letter multipliers are
    used.  For tiles on the board neither the word nor letter multipliers are
    used.

    Because position could be in the middle of a word, the word multipliers
    aren't applied, but instead are returned as part of the tuple.

    The actual position itself isn't counted because depending on external
    circumstances it may or may not be the correct thing to do.
    """
    if direction[0] == 0 and direction[1] == 0:
      # If our direction is stationary we will never finish.
      return (0, 1)

    score = 0
    word_multipliers = 1
    letters_used = []

    # Advance one first so that we don't count where we start.
    pos = list((position[0] + direction[0],
                position[1] + direction[1]))

    # Iterate through the tiles on the board from this point.
    try:
      while not self[pos].Empty() or move.UsesPosition(pos):
        if not self[pos].Empty():
          # There was already a tile on the board.  Add its value but ignore any
          # letter or word multipliers for that space.
          score += Board.score_for_letter[self[pos].Letter()]
          letters_used.append(self[pos].Letter())
        else:
          # There's no tile currently there, but there is one in our move.
          # Since this would be placed on the board, we do count letter and word
          # multipliers.
          score += (Board.score_for_letter[move.LetterAtPosition(pos)] *
                    self[pos].LetterMultiplier())
          word_multipliers *= self[pos].WordMultiplier()
          letters_used.append(move.LetterAtPosition(pos))

        # Advance!
        pos[0] += direction[0]
        pos[1] += direction[1]
    except IllegalBoardPositionError:
      # If we fall off the end of the board that just means we're done.
      pass

    return (score, word_multipliers, "".join(letters_used))

  def _SetSymmetricalMultipliers(self, location, word, letter):
    """For each location, sets the 8-way mirrored locations as well.

    Given a location (x,y), this will set the multiplication parameters for the
    piece at that location, as well as that location mirrored over the diagonal,
    and it will do this in each of the 4 quadrants of the board.
    """
    # Make an array out of the tuple so that it's mutable.
    loc = []
    loc.append(location[0])
    loc.append(location[1])

    # There are 8 operations done.  The board is mirrored in triangles.
    # Do the upper right quandrant.
    self._SetMultipliers((loc[0], loc[1]), word, letter)
    self._SetMultipliers((loc[1], loc[0]), word, letter)

    # Next, the upper left quadrant.
    loc[0] *= -1
    self._SetMultipliers((loc[0], loc[1]), word, letter)
    self._SetMultipliers((loc[1], loc[0]), word, letter)

    # Lower left.
    loc[1] *= -1
    self._SetMultipliers((loc[0], loc[1]), word, letter)
    self._SetMultipliers((loc[1], loc[0]), word, letter)

    # Lower right.
    loc[0] *= -1
    self._SetMultipliers((loc[0], loc[1]), word, letter)
    self._SetMultipliers((loc[1], loc[0]), word, letter)

  def _SetMultipliers(self, location, word = 1, letter = 1):
    """Sets the multiplications of a piece in board-space.

    Given a position in board-space, where 0,0 corresponds to the star at the
    center of the board, set the multiplications of that square.
    """
    piece = self[location]
    piece.SetWordMultiplier(word)
    piece.SetLetterMultiplier(letter)

  def _FromBoardSpace(self, location):
    """Transforms from a board-space (x,y) position to an array-space one.

    Checks that the input location is valid, and throws an
    IllegalBoardPositionError if it isn't valid.  An invalid location is one
    where either access isn't between -7 and 7 (inclusive).

    Args:
      location: An (x,y) position to be translated.

    Returns:
      An (x,y) tuple suitable for indexing into the board array.
    """
    if not (-7 <= location[0] <= 7 and -7 <= location[1] <= 7):
      raise IllegalBoardPositionError(location)
    return (7 - location[1], 7 + location[0])

  def _SquaresAroundPosition(self, position):
    """Returns a list of any non-empty BoardSquares around position.
    
    Args:
      position: An (x,y) tuple.

    Returns:
      A potentially empty list of non-empty BoardSquares around position.
    """
    pos = list(position)
    squares = []
    for x,y in [(0,1), (0,-1), (1,0), (-1,0)]:
      try:
        square = self[pos[0] + x, pos[1] + y]
      except IllegalBoardPositionError:
        pass
      else:
        if not square.Empty():
          squares.append(square)
    return squares

  def __getitem__(self, position):
    """Returns the BoardSquare at position (x,y).

    Raises a TypeError if position is not an (x,y) tuple or list.

    Args:
      position: An (x,y) tuple.

    Returns:
      A BoardSquare from the specified position.
    """
    # Check that we have an iterable of length 2 to index with.
    if len(position) != 2:
      raise TypeError("Board indices must be (x,y) pairs.")

    # Convert to array-space.
    pos = self._FromBoardSpace(position)
    return self.board[pos[0]][pos[1]]

  def __str__(self):
    to_return = []
    for row in self.board:
      s = []
      for piece in row:
        s.append(str(piece))
      to_return.append("".join(s))
    return "\n".join(to_return)

  # Data members:
  score_for_letter = {
      "A" : 1,
      "B" : 3,
      "C" : 3,
      "D" : 2,
      "E" : 1,
      "F" : 4,
      "G" : 2,
      "H" : 4,
      "I" : 1,
      "J" : 8,
      "K" : 5,
      "L" : 1,
      "M" : 3,
      "N" : 1,
      "O" : 1,
      "P" : 3,
      "Q" : 10,
      "R" : 1,
      "S" : 1,
      "T" : 1,
      "U" : 1,
      "V" : 4,
      "W" : 4,
      "X" : 8,
      "Y" : 4,
      "Z" : 10,
      BLANK_TILE : 0}

class BoardTest(unittest.TestCase):
  def ScoreAlongMove(self, board, move):
    """Helper function for scoring along a move."""
    return board._ScorePosition(move, move.Position(), move.Direction())

  def WordScore(self, word):
    """Returns the sum of the score for each tile."""
    return sum([Board.score_for_letter[letter] for letter in word])

  def testBoardCreation(self):
    b = Board()
    self.assert_(len(b.board) == 15, "%d." % len(b.board))
    for row in b.board:
      self.assert_(len(row) == 15)

  def testEmptyBoard(self):
    b = Board()
    self.assert_(b.Empty())

  def testNonEmptyBoard(self):
    b = Board()
    b[(0,0)].SetLetter("A")
    self.assert_(not b.Empty())

  def testRandomPosition(self):
    b = Board()
    self.assert_(b.RandomPosition() == None)

    b[(2,-2)].SetLetter("A")
    self.assert_(b[b.RandomPosition()].Letter() == "A")

  def testRandomPiece(self):
    b = Board()
    b[(2,-2)].SetLetter("A")
    self.assert_(b.RandomPiece().Letter() == "A")

  def testValidPositions(self):
    b = Board()
    b[(0,0)].SetLetter("A")
    b[(1,0)].SetLetter("A")
    
    positions = b.ValidPositions()
    self.assert_((0,1) in positions)
    self.assert_((-1,0) in positions)
    self.assert_((2,0) in positions)
    self.assert_((0,-1) in positions)
    self.assert_((1,-1) in positions)
    self.assert_((1,1) in positions)
    self.assert_(len(positions) == 6)

  def testDirectionsOfPieces(self):
    b = Board()
    self.assert_(b.LeftOfPosition((-7,0)) == None)
    self.assert_(b.RightOfPosition((7,0)) == None)
    self.assert_(b.AbovePosition((0,7)) == None)
    self.assert_(b.BelowPosition((0,-7)) == None)

    b[(0,0)].SetLetter("A")
    self.assert_(b.LeftOfPosition((1,0)).Letter() == "A")
    self.assert_(b.RightOfPosition((-1,0)).Letter() == "A")
    self.assert_(b.AbovePosition((0,-1)).Letter() == "A")
    self.assert_(b.BelowPosition((0,1)).Letter() == "A")

  def testFirstMoveDoesntCoverZeroZero(self):
    b = Board()
    m = Move()
    m.AddLetterAtPosition("A", (1,1))
    self.assertRaises(InvalidMoveError, b.ScoreForMove, m)

  def testFirstMoveDoesCoverZeroZero(self):
    b = Board()
    m = Move()
    m.AddLetterAtPosition("A", (0,0))
    m.AddLetterAtPosition("A", (1,0))
    score = b.ScoreForMove(m)
    self.assert_(score == 4, "Got %d." % score)

  def testSquaresAroundCenter(self):
    b = Board()
    b[(0,0)].SetLetter("A")
    b[(0,1)].SetLetter("B")
    b[(1,1)].SetLetter("C")
    b[(1,0)].SetLetter("D")

    squares = b._SquaresAroundPosition((0,0))
    self.assert_(len(squares) == 2, "%d" % len(squares))
    
    letters = [square.Letter() for square in squares]
    self.assert_("B" in letters, repr(letters))
    self.assert_("D" in letters, repr(letters))
  
  def testSquaresAroundPositionNearEdge(self):
    b = Board()
    b[(-7, -7)].SetLetter("A")
    b[(-6, -7)].SetLetter("B")
    b[(-6, -6)].SetLetter("C")

    squares = b._SquaresAroundPosition((-7, -7))
    self.assert_(len(squares) == 1, "%d" % len(squares))
    
    letters = [square.Letter() for square in squares]
    self.assert_("B" in letters, repr(letters))

  def testScoringInvalidMove(self):
    b = Board()
    m = Move()
    m.valid = False
    self.assertRaises(InvalidMoveError, b.ScoreForMove, m)

  def testScoringMoveOnTopOfPreviousMove(self):
    b = Board()
    b[(0,0)].SetLetter("A")

    m = Move()
    m.AddLetterAtPosition("B", (0,0))
    self.assertRaises(InvalidMoveError, b.ScoreForMove, m)

  def testScoringMoveThatDoesntTouchAnotherMove(self):
    b = Board()
    b[(0,0)].SetLetter("A")
    b[(1,0)].SetLetter("A")
    b[(2,0)].SetLetter("A")
    b[(3,0)].SetLetter("A")

    m = Move()
    m.AddLetterAtPosition("B", (5,5))
    m.AddLetterAtPosition("B", (5,6))
    m.AddLetterAtPosition("B", (5,7))

    self.assertRaises(InvalidMoveError, b.ScoreForMove, m)

  def testScoringWordMultipliers(self):
    # Add a piece at (0,0) and (6,1) to appease the rules.
    b = Board()
    b[(0,0)].SetLetter("A")
    b[(6,1)].SetLetter("A")

    m = Move()
    m.AddLetterAtPosition("B", (7,1))
    m.AddLetterAtPosition("A", (7,0))  # Triple word score.

    # A is 1 point and B is 3 points, so this should score 4 * 3 + (3 + 1 for BA
    # horizontally) = 16.
    score = b.ScoreForMove(m)
    self.assert_(score == 16, "Got %d" % score)

  def testScoringLetterMultiplier(self):
    # Add a piece at (0,0) to appease the rules.
    b = Board()
    b[(0,0)].SetLetter("A")

    m = Move()
    m.AddLetterAtPosition("A", (1,1))
    m.AddLetterAtPosition("B", (1,0))
    m.AddLetterAtPosition("C", (1,-1))

    # A is 1 point, B is 3, and C is 3.  A and C are on triple letter squares,
    # so the final score should be 1 * 2 + 3 + 3 * 2 ( + 4 for AB horizontally)
    # = 15.
    score = b.ScoreForMove(m)
    self.assert_(score == 15, "Got %d" % score)

  def testScoringWordAndLetterMultipliers(self):
    # Add a piece at (0,0) and (6,-7) to appease the rules.
    b = Board()
    b[(0,0)].SetLetter("A")
    b[(6,-7)].SetLetter("A")

    m = Move()
    m.AddLetterAtPosition("A", (7,-4))
    m.AddLetterAtPosition("B", (7,-5))
    m.AddLetterAtPosition("C", (7,-6))
    m.AddLetterAtPosition("D", (7,-7))

    # A is 1 point, B is 3, C is 3, and D is 2.  A is on a double letter and D
    # is on a triple word, so the final score should be
    # (1 * 2 + 3 + 3 + 2) * 3 + ((1 + 2) * 3 for horizontal AD)= 39
    score = b.ScoreForMove(m)
    self.assert_(score == 39, "Got %d" % score)

  def testAddingAPieceNotTouchingAnyOthers(self):
    b = Board()
    b[(0,0)].SetLetter("A")

    m = Move()
    m.AddLetterAtPosition("B", (1,1))
    self.assertRaises(InvalidMoveError, b.ScoreForMove, m)
    self.assert_(not m.Valid())

    m.Clear()
    m.AddLetterAtPosition("B", (0,1))
    score = b.ScoreForMove(m)
    self.assert_(score == 4, "Got %d." % score)

  def testAddingPiecesThatDontFormAHorizontalLine(self):
    # If the board is [A] [ ] [C] [ ] [ ], it's okay to turn it into:
    # [A] [B] [C] [D] [ ] but not [A] [B] [C] [ ] [D].
    b = Board()
    b[(0,0)].SetLetter("A")
    b[(2,0)].SetLetter("C")

    m = Move()
    m.AddLetterAtPosition("B", (1,0))
    m.AddLetterAtPosition("D", (3,0))

    # This is an acceptable move.
    score = b.ScoreForMove(m)
    self.assert_(score == 9, "Got %d." % score)

    # Try scoring an unacceptable move (one that has gaps).
    m.Clear()
    m.AddLetterAtPosition("B", (1,0))
    m.AddLetterAtPosition("D", (5,0))
    self.assertRaises(InvalidMoveError, b.ScoreForMove, m)

  def testAddingPiecesThatDontFormAVerticalLine(self):
    b = Board()
    b[(0,0)].SetLetter("A")
    b[(0,2)].SetLetter("C")

    m = Move()
    m.AddLetterAtPosition("B", (0,1))
    m.AddLetterAtPosition("D", (0,3))

    # This is an acceptable move.
    score = b.ScoreForMove(m)

    # Try scoring an unacceptable move (one that has gaps).
    m.Clear()
    m.AddLetterAtPosition("B", (0,1))
    m.AddLetterAtPosition("D", (0,5))
    self.assertRaises(InvalidMoveError, b.ScoreForMove, m)

  def testAddingASinglePieceInBetweenOthers(self):
    b = Board()
    b[(0,0)].SetLetter("A")
    b[(2,0)].SetLetter("A")

    m = Move()
    m.AddLetterAtPosition("B", (1,0))

    score = b.ScoreForMove(m)
    self.assert_(score == 5, "Got %d." % score)

  def testSumOfTilesFromPosInDirHorizontal(self):
    b = Board()
    b[(0,0)].SetLetter("G")
    b[(2,0)].SetLetter("B")

    m = Move()
    m.AddLetterAtPosition("A", (1,0))
    m.AddLetterAtPosition("E", (3,0))

    # G = 2, A = 1, B = 3, E = 1.  Depending on which space we start on, we
    # should get a different score because the starting space is left out of the
    # final score.
    starting_spaces = {(0,0) : 5, (1,0) : 6, (2,0) : 4, (3,0) : 6}
    forward_words   = {(0,0) : "ABE", (1,0) : "BE", (2,0) : "E", (3,0) : ""}
    backwards_words = {(0,0) : "", (1,0) : "G", (2,0) : "AG", (3,0) : "BAG"}

    for start, true_score in starting_spaces.iteritems():
      f_score, f_word_mult, f_letters = b._SumOfTilesFromPositionInDirection(
          m, start, (1,0))
      b_score, b_word_mult, b_letters = b._SumOfTilesFromPositionInDirection(
          m, start, (-1,0))
      score = f_score + b_score
      mult = f_word_mult * b_word_mult
      self.assert_(score == true_score, "Got %d." % score)
      self.assert_(mult == 1, "Got %d." % mult)
      self.assert_(f_letters == forward_words[start], "Got %s." % f_letters)
      self.assert_(b_letters == backwards_words[start], "Got %s." % b_letters)
      
  def testSumOfTilesFromPosInDirVertical(self):
    b = Board()
    b[(0,0)].SetLetter("G")
    b[(0,-2)].SetLetter("B")

    m = Move()
    m.AddLetterAtPosition("A", (0,-1))
    m.AddLetterAtPosition("E", (0,-3))

    # G = 2, A = 1, B = 3, E = 1.  Depending on which space we start on, we
    # should get a different score because the starting space is left out of the
    # final score.
    starting_spaces = {(0,0) : 5, (0,-1) : 6, (0,-2) : 4, (0,-3) : 6}
    forward_words   = {(0,0) : "ABE", (0,-1) : "BE", (0,-2) : "E", (0,-3) : ""}
    backwards_words = {(0,0) : "", (0,-1) : "G", (0,-2) : "AG", (0,-3) : "BAG"}

    for start, true_score in starting_spaces.iteritems():
      f_score, f_word_mult, f_letters = b._SumOfTilesFromPositionInDirection(
          m, start, (0,-1))
      b_score, b_word_mult, b_letters = b._SumOfTilesFromPositionInDirection(
          m, start, (0,1))
      score = f_score + b_score
      mult = f_word_mult * b_word_mult
      self.assert_(score == true_score, "Got %d." % score)
      self.assert_(mult == 1, "Got %d." % mult)
      self.assert_(f_letters == forward_words[start],
                   "Got %s starting at %s." % (f_letters, repr(start)))
      self.assert_(b_letters == backwards_words[start], "Got %s." % b_letters)
      self.assert_(f_letters == forward_words[start], "Got %s." % f_letters)
      self.assert_(b_letters == backwards_words[start], "Got %s." % b_letters)

  def testSumOfTilesFromPosInDirWithZeroDirection(self):
    b = Board()
    b[(0,0)].SetLetter("A")

    m = Move()
    m.AddLetterAtPosition("B", (1,0))

    score, word_mult = b._SumOfTilesFromPositionInDirection(m, (1,0), (0,0))
    self.assert_(score == 0, "Got %d." % score)
    self.assert_(word_mult == 1, "Got %d." % word_mult)

  def testSumOfTilesFromSingleTileMove(self):
    b = Board()
    b[(0,0)].SetLetter("A")

    m = Move()
    m.AddLetterAtPosition("B", (1,0))

    score, word_mult, letters = b._SumOfTilesFromPositionInDirection(m,
                                                                     (1,0),
                                                                     (-1,0))
    self.assert_(score == 1, "Got %d." % score)
    self.assert_(word_mult == 1, "Got %d." % word_mult)
    self.assert_(letters == "A", "Got %s." % letters)

  def testScoreAlongMoveHorizontal(self):
    b = Board()
    b[(0,0)].SetLetter("G")
    b[(2,0)].SetLetter("B")

    m = Move()
    m.AddLetterAtPosition("A", (1,0))
    m.AddLetterAtPosition("E", (3,0))

    # G = 2, A = 1, B = 3, E = 1.  The double word at (0,0) is already used, so
    # our final score for this move should be 7.
    score = self.ScoreAlongMove(b, m)
    self.assert_(score == 7, "Got %d." % score)

  def testScoreAlongMoveVertical(self):
    b = Board()
    b[(0,0)].SetLetter("G")
    b[(0,2)].SetLetter("B")

    m = Move()
    m.AddLetterAtPosition("A", (0,1))
    m.AddLetterAtPosition("E", (0,3))

    # G = 2, A = 1, B = 3, E = 1.  The double word at (0,0) is already used, so
    # our final score for this move should be 7.
    score = self.ScoreAlongMove(b, m)
    self.assert_(score == 7, "Got %d." % score)

  def testScoreAlongMoveSinglePieceAfterHorizontalWord(self):
    b = Board()
    b[(0,0)].SetLetter("C")
    b[(1,0)].SetLetter("A")
    
    m = Move()
    m.AddLetterAtPosition("T", (2,0))

    # Single piece moves are considered to be vertical, so it should have 0
    # score in the vertical direction.
    score = self.ScoreAlongMove(b, m)
    self.assert_(score == 0, "Got %d." % score)

  def testScoreAlongMoveSinglePieceBeforeHorizontalWord(self):
    b = Board()
    b[(0,0)].SetLetter("C")
    b[(1,0)].SetLetter("A")
    
    m = Move()
    m.AddLetterAtPosition("T", (-1,0))

    # Single piece moves are considered to be vertical, so it should have 0
    # score in the vertical direction.
    score = self.ScoreAlongMove(b, m)
    self.assert_(score == 0, "Got %d." % score)

  def testScoreAlongMoveSinglePieceAfterVerticalWord(self):
    b = Board()
    b[(0,0)].SetLetter("C")
    b[(0,1)].SetLetter("A")
    
    m = Move()
    m.AddLetterAtPosition("T", (0,2))

    score = self.ScoreAlongMove(b, m)
    self.assert_(score == 5, "Got %d." % score)

  def testScoreAlongMoveSinglePieceBeforeVerticalWord(self):
    b = Board()
    b[(0,0)].SetLetter("C")
    b[(0,1)].SetLetter("A")
    
    m = Move()
    m.AddLetterAtPosition("T", (0,-1))

    score = self.ScoreAlongMove(b, m)
    self.assert_(score == 5, "Got %d." % score)

  def testScoreAlongMoveHorizontalEitherEndWithDoubleWord(self):
    b = Board()
    b[(1,0)].SetLetter("B")
    b[(2,0)].SetLetter("C")

    m = Move()
    m.AddLetterAtPosition("A", (0,0))
    m.AddLetterAtPosition("D", (3,0))

    # Final score should be (1 + 3 + 3 + 2) * 2 = 18
    score = self.ScoreAlongMove(b, m)
    self.assert_(score == 18, "Got %d." % score)

  def testScoreAlongMoveHorizontalTripleTriple(self):
    b = Board()
    b[(1,-7)].SetLetter("A")
    b[(2,-7)].SetLetter("A")
    b[(3,-7)].SetLetter("A")
    b[(4,-7)].SetLetter("A")
    b[(5,-7)].SetLetter("A")
    b[(6,-7)].SetLetter("A")

    m = Move()
    m.AddLetterAtPosition("A", (0,-7))
    m.AddLetterAtPosition("A", (7,-7))

    score = b.ScoreForMove(m)
    self.assert_(score == 3 * 3 * 8, "Got %d." % score)

  def testScoreForMove(self):
    # Set up CAT and HAT in the corner.
    b = Board()
    b[(4,-7)].SetLetter("C")
    b[(5,-7)].SetLetter("A")
    b[(6,-7)].SetLetter("T")

    b[(7,-4)].SetLetter("H")
    b[(7,-5)].SetLetter("A")
    b[(7,-6)].SetLetter("T")

    m = Move()
    m.AddLetterAtPosition("S", (7,-7))

    # We should get CATS = (3 + 1 + 1 + 1) * 3 = 18
    # and           HATS = (4 + 1 + 1 + 1) * 3 = 21
    # equals                                   = 39
    score = b.ScoreForMove(m)
    self.assert_(score == 39, "Got %d." % score)

  def testScoreForMoveHorizontalThroughVertical(self):
    # We're testing the following case:
    #         _G_
    #         _A_
    # [G] [A] _B_ [E]
    #         _E_
    b = Board()
    b[(0,2)].SetLetter("G")
    b[(0,1)].SetLetter("A")
    b[(0,0)].SetLetter("B")
    b[(0,-1)].SetLetter("E")

    m = Move()
    m.AddLetterAtPosition("G", (-2, 0))
    m.AddLetterAtPosition("A", (-1, 0))
    m.AddLetterAtPosition("E", (1, 0))

    # We should get G + A + B + E.
    score = b.ScoreForMove(m)
    real_score = self.WordScore("GABE")
    self.assert_(score == real_score,
                 "Got %d.  Should have %d." % (score, real_score))

  def testScoreForMoveVerticalThroughHorizontal(self):
    # We're testing the following case:
    #         [G]
    #         [A]
    # _G_ _A_ _B_ _E_
    #         [E]
    b = Board()
    b[(-2,0)].SetLetter("G")
    b[(-1,0)].SetLetter("A")
    b[(0,0)].SetLetter("B")
    b[(1,0)].SetLetter("E")

    m = Move()
    m.AddLetterAtPosition("G", (0, 2))
    m.AddLetterAtPosition("A", (0, 1))
    m.AddLetterAtPosition("E", (0, -1))

    # We should get G + A + B + E.
    score = b.ScoreForMove(m)
    real_score = self.WordScore("GABE")
    self.assert_(score == real_score,
                 "Got %d.  Should have %d." % (score, real_score))

  def testScoreForMoveWithComplicatedExample(self):
    # We're going for this:
    #     _G_
    #     _A_     _C_
    #     _B_     _A_
    # _H_ [E] [A] [R]
    b = Board()
    b[(4,-7)].SetLetter("H")

    b[(5,-4)].SetLetter("G")
    b[(5,-5)].SetLetter("A")
    b[(5,-6)].SetLetter("B")

    b[(7,-5)].SetLetter("C")
    b[(7,-6)].SetLetter("A")

    m = Move()
    m.AddLetterAtPosition("E", (5, -7))
    m.AddLetterAtPosition("A", (6, -7))
    m.AddLetterAtPosition("R", (7, -7))

    score = b.ScoreForMove(m)
    real_score = (self.WordScore("HEAR") * 3 +
                  self.WordScore("CAR") * 3 +
                  self.WordScore("GABE"))
    self.assert_(score == real_score,
                 "Got %d. Should have %d." % (score, real_score))

  def testScoringWordInWordList(self):
    b = Board(WordList(["GABE"]))

    m = Move()
    m.AddLetterAtPosition("G", (0,0))
    m.AddLetterAtPosition("A", (1,0))
    m.AddLetterAtPosition("B", (2,0))
    m.AddLetterAtPosition("E", (3,0))
    
    score = b.ScoreForMove(m)
    self.assert_(score != 0, "Got %d." % score)

  def testScoringWordNotInWordList(self):
    b = Board(WordList(["GABE"]))

    m = Move()
    m.AddLetterAtPosition("G", (0,0))
    m.AddLetterAtPosition("I", (1,0))
    m.AddLetterAtPosition("B", (2,0))
    m.AddLetterAtPosition("E", (3,0))
    
    self.assertRaises(InvalidMoveError, b.ScoreForMove, m)

  def testPlayingWordInListButGettingPointsForOneNot(self):
    #                 _H_
    #                 _A_
    #                 _T_
    # _G_ _A_ _B_ _E_ [S]
    #
    # "GABES" is not a word.
    b = Board(WordList(["GABE", "HAT", "HATS"]))
    b[(0,0)].SetLetter("G")
    b[(1,0)].SetLetter("A")
    b[(2,0)].SetLetter("B")
    b[(3,0)].SetLetter("E")

    b[(4,3)].SetLetter("H")
    b[(4,2)].SetLetter("A")
    b[(4,1)].SetLetter("T")

    m = Move()
    m.AddLetterAtPosition("S", (4,0))

    self.assertRaises(InvalidMoveError, b.ScoreForMove, m)

  def testBingoBoost(self):
    b = Board()

    m = Move()
    m.AddLetterAtPosition("A", (0,0))
    m.AddLetterAtPosition("A", (1,0))
    m.AddLetterAtPosition("A", (2,0))
    m.AddLetterAtPosition("A", (3,0))
    m.AddLetterAtPosition("A", (4,0))
    m.AddLetterAtPosition("A", (5,0))
    m.AddLetterAtPosition("A", (6,0))

    score = b.ScoreForMove(m)
    self.assert_(score == 66, "Got %d." % score)

  def testBingoBoostOnInvalidWord(self):
    b = Board(WordList(["GABE"]))

    m = Move()
    m.AddLetterAtPosition("A", (0,0))
    m.AddLetterAtPosition("A", (1,0))
    m.AddLetterAtPosition("A", (2,0))
    m.AddLetterAtPosition("A", (3,0))
    m.AddLetterAtPosition("A", (4,0))
    m.AddLetterAtPosition("A", (5,0))
    m.AddLetterAtPosition("A", (6,0))

    self.assertRaises(InvalidMoveError, b.ScoreForMove, m)

  def testChangingWordMultipliers(self):
    b = Board()
    pre = b.BoardHash()
    b[(0,0)].SetWordMultiplier(10)
    post = b.BoardHash()
    self.assert_(pre != post)

  def testChangingLetterMultipliers(self):
    b = Board()
    pre = b.BoardHash()
    b[(0,0)].SetLetterMultiplier(10)
    post = b.BoardHash()
    self.assert_(pre != post)

  def testChangingLetters(self):
    b = Board()
    pre = b.BoardHash()
    b[(0,0)].SetLetter("A")
    post = b.BoardHash()
    self.assert_(pre != post)

if __name__ == '__main__':
  unittest.main()
